/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */
package software.amazon.smithy.cli.commands;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import software.amazon.smithy.build.model.MavenRepository;
import software.amazon.smithy.cli.CliError;
import software.amazon.smithy.cli.dependencies.DependencyResolverException;
import software.amazon.smithy.cli.dependencies.ResolvedArtifact;
import software.amazon.smithy.model.node.ArrayNode;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.node.ToNode;
import software.amazon.smithy.utils.BuilderRef;
import software.amazon.smithy.utils.IoUtils;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.SmithyBuilder;
import software.amazon.smithy.utils.ToSmithyBuilder;

final class LockFile implements ToSmithyBuilder<LockFile>, ToNode {
    private static final Path LOCK_FILE = Paths.get("smithy-lock.json");
    private static final String DEFAULT_VERSION = "1.0";
    private static final String WARNING_STR = "AUTOGENERATED_DO_NOT_MODIFY";
    private static final String VERSION_NAME = "version";
    private static final String CONFIG_HASH_NAME = "configHash";
    private static final String ARTIFACT_LIST_NAME = "artifacts";
    private static final String REPOSITORIES_LIST_NAME = "repositories";
    private static final List<String> EXPECTED_PROPERTIES = ListUtils.of(
            WARNING_STR,
            VERSION_NAME,
            CONFIG_HASH_NAME,
            ARTIFACT_LIST_NAME,
            REPOSITORIES_LIST_NAME);

    private final Map<String, LockedArtifact> artifacts;
    private final Set<String> repositories;
    private final int configHash;
    private final String version;

    private LockFile(Builder builder) {
        this.artifacts = builder.artifacts.copy();
        this.repositories = builder.repositories.copy();
        this.version = builder.version;
        this.configHash = builder.configHash;
    }

    public static LockFile fromNode(Node node) {
        LockFile.Builder builder = builder();
        node.expectObjectNode()
                .warnIfAdditionalProperties(EXPECTED_PROPERTIES)
                .getStringMember(VERSION_NAME, builder::version)
                .getNumberMember(CONFIG_HASH_NAME, n -> builder.configHash(n.intValue()))
                .getObjectMember(ARTIFACT_LIST_NAME, artifactList -> {
                    for (Map.Entry<String, Node> entry : artifactList.getStringMap().entrySet()) {
                        builder.artifact(LockedArtifact.fromCoordinateNode(entry.getKey(), entry.getValue()));
                    }
                })
                .getArrayMember(REPOSITORIES_LIST_NAME, StringNode::getValue, builder::repositories);
        return builder.build();
    }

    public static Optional<LockFile> load() {
        return load(LOCK_FILE);
    }

    /**
     * Loads lockfile from path if it exists.
     *
     * @param path path of file to load as a lockfile
     * @return Lockfile if it exists otherwise Optional.empty().
     */
    public static Optional<LockFile> load(Path path) {
        if (Files.exists(path) && Files.isRegularFile(path)) {
            String contents = IoUtils.readUtf8File(path);
            Node result = Node.parseJsonWithComments(contents);
            return Optional.of(LockFile.fromNode(result));
        }
        return Optional.empty();
    }

    @Override
    public Node toNode() {
        ObjectNode.Builder builder = Node.objectNodeBuilder()
                .withMember(WARNING_STR, "")
                .withMember(VERSION_NAME, version)
                .withMember(CONFIG_HASH_NAME, configHash)
                .withMember(REPOSITORIES_LIST_NAME, ArrayNode.fromStrings(repositories));

        ObjectNode.Builder artifactNodeBuilder = Node.objectNodeBuilder();
        for (LockedArtifact artifact : artifacts.values()) {
            artifactNodeBuilder.withMember(artifact.coordinates, artifact.toNode());
        }
        builder.withMember(ARTIFACT_LIST_NAME, artifactNodeBuilder.build());

        return builder.build();
    }

    public void validateArtifacts(List<ResolvedArtifact> resolvedArtifacts) {
        for (ResolvedArtifact artifact : resolvedArtifacts) {
            LockedArtifact pin = artifacts.get(artifact.getCoordinates());
            if (pin == null) {
                throw new CliError("Resolved artifact `" + artifact + "` not found in locked artifacts.");
            }
            if (!pin.matchesResolved(artifact)) {
                throw new CliError("Resolved artifact `" + artifact + "` does not match locked artifact `" + pin + "`");
            }
        }
    }

    public static LockFile.Builder builder() {
        return new LockFile.Builder();
    }

    /**
     * @return Gets the configuration hash for the lockfile
     */
    public int getConfigHash() {
        return configHash;
    }

    /**
     * @return Gets the list of pinned artifacts.
     */
    public Set<String> getDependencyCoordinateSet() {
        return artifacts.keySet();
    }

    /**
     * @return Gets the version of lockfile.
     */
    public String getVersion() {
        return version;
    }

    /**
     * @return Gets the pinned repositories in the lockfile.
     */
    public Set<String> getRepositories() {
        return repositories;
    }

    /**
     * Saves lockfile to current working directory.
     */
    public void save() {
        try {
            Files.write(LOCK_FILE, Node.printJson(this.toNode()).getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public LockFile.Builder toBuilder() {
        return builder().repositories(repositories)
                .artifacts(artifacts)
                .version(version)
                .configHash(configHash);
    }

    public static final class Builder implements SmithyBuilder<LockFile> {
        private final BuilderRef<Map<String, LockedArtifact>> artifacts = BuilderRef.forOrderedMap();
        private final BuilderRef<Set<String>> repositories = BuilderRef.forOrderedSet();
        private String version = DEFAULT_VERSION;
        private int configHash;

        public LockFile.Builder artifacts(Map<String, LockedArtifact> artifacts) {
            this.artifacts.clear();
            this.artifacts.get().putAll(artifacts);
            return this;
        }

        public LockFile.Builder artifacts(Collection<ResolvedArtifact> artifacts) {
            this.artifacts.clear();
            for (ResolvedArtifact artifact : artifacts) {
                this.artifacts.get().put(artifact.getCoordinates(), LockedArtifact.from(artifact));
            }
            return this;
        }

        public LockFile.Builder artifact(ResolvedArtifact artifact) {
            this.artifacts.get().put(artifact.getCoordinates(), LockedArtifact.from(artifact));
            return this;
        }

        public LockFile.Builder artifact(LockedArtifact artifact) {
            this.artifacts.get().put(artifact.coordinates, artifact);
            return this;
        }

        public LockFile.Builder repositories(Set<MavenRepository> repositories) {
            this.repositories.clear();
            for (MavenRepository repository : repositories) {
                this.repositories.get().add(repository.getUrl());
            }
            return this;
        }

        public LockFile.Builder repositories(Collection<String> repositories) {
            this.repositories.clear();
            this.repositories.get().addAll(repositories);
            return this;
        }

        public LockFile.Builder version(String version) {
            this.version = version;
            return this;
        }

        public LockFile.Builder configHash(int configHash) {
            this.configHash = configHash;
            return this;
        }

        @Override
        public LockFile build() {
            return new LockFile(this);
        }
    }

    private static final class LockedArtifact implements ToNode {
        private static final String SHA_SUM_MEMBER_NAME = "sha1";
        private final String coordinates;
        private final String shaSum;

        private LockedArtifact(String coordinates, String shaSum) {
            this.coordinates = coordinates;
            this.shaSum = shaSum;
        }

        private static LockedArtifact from(ResolvedArtifact resolvedArtifact) {
            return new LockedArtifact(resolvedArtifact.getCoordinates(), resolvedArtifact.getShaSum());
        }

        private static LockedArtifact fromCoordinateNode(String coordinates, Node node) {
            String[] parts = coordinates.split(":");
            if (parts.length != 3) {
                throw new DependencyResolverException("Invalid Locked coordinates: " + coordinates);
            }
            ObjectNode objectNode = node.expectObjectNode();
            String shaSum = objectNode.expectMember(SHA_SUM_MEMBER_NAME).expectStringNode().getValue();

            return new LockedArtifact(coordinates, shaSum);
        }

        @Override
        public Node toNode() {
            return Node.objectNodeBuilder()
                    .withMember(SHA_SUM_MEMBER_NAME, shaSum)
                    .build();
        }

        boolean matchesResolved(ResolvedArtifact resolved) {
            return shaSum.equals(resolved.getShaSum());
        }
    }
}
